from itertools import chain
from typing import Callable, Iterable, List

from common.credentials import (
    Credentials,
    CredentialsComponent,
    Identity,
    Password,
    Secret,
    Username,
)
from infection_monkey.exploit.tools import identity_type_filter, secret_type_filter


def generate_community_strings(credentials: Iterable[Credentials]) -> Iterable[str]:
    """
    Yields community strings from credentials

    :param credentials: The credentials from which to generate community strings
    :return: An iterable of potential community strings
    """
    for credential in _select_credentials_components(
        credentials,
        identity_type_filter=identity_type_filter([Username]),
        secret_type_filter=secret_type_filter([Password]),
    ):
        yield _credentials_component_to_community_string(credential)


def _select_credentials_components(
    input_credentials: Iterable[Credentials],
    identity_type_filter: Callable[[Identity], bool] = lambda identity: True,
    secret_type_filter: Callable[[Identity], bool] = lambda secret: True,
) -> Iterable[CredentialsComponent]:
    identities: List[Identity] = []
    secrets: List[Secret] = []

    for credentials in input_credentials:
        if credentials.identity is not None:
            identities.append(credentials.identity)
        if credentials.secret is not None:
            secrets.append(credentials.secret)

    for credential in chain(
        filter(identity_type_filter, _deduplicate(identities)),
        filter(secret_type_filter, _deduplicate(secrets)),
    ):
        yield credential


def _deduplicate(iterable: Iterable[CredentialsComponent]) -> Iterable[CredentialsComponent]:
    # Using dict.fromkeys() instead of set() because dicts preserve order
    return dict.fromkeys(iterable).keys()


def _credentials_component_to_community_string(credential: CredentialsComponent) -> str:
    if isinstance(credential, Username):
        return credential.username
    elif isinstance(credential, Password):
        return credential.password.get_secret_value()
    else:
        raise TypeError(f"Unexpected credential type: {type(credential)}")
